package com.nineclock.attendance.service.impl;

import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nineclock.attendance.dto.AttendGroupDTO;
import com.nineclock.attendance.dto.AttendPunchDTO;
import com.nineclock.attendance.dto.AttendsDTO;
import com.nineclock.attendance.enums.AttendEnums;
import com.nineclock.attendance.excel.CustomHandler;
import com.nineclock.attendance.mapper.AttendPunchMapper;
import com.nineclock.attendance.pojo.AttendPunch;
import com.nineclock.attendance.service.AttendGroupService;
import com.nineclock.attendance.service.AttendPunchService;
import com.nineclock.common.enums.ResponseEnum;
import com.nineclock.common.exception.NcException;
import com.nineclock.common.filter.CurrentUserHolder;
import com.nineclock.common.utils.BeanHelper;
import com.nineclock.common.utils.DateTimeUtil;
import com.nineclock.common.utils.DistanceUtil;
import com.nineclock.system.dto.SysCompanyUserDTO;
import com.nineclock.system.feign.SysCompanyUserFeign;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Transactional
public class AttendPunchServiceImpl implements AttendPunchService {

    @Autowired
    private AttendPunchMapper attendPunchMapper;

    @Autowired
    private AttendGroupService attendGroupService;

    @Autowired
    private SysCompanyUserFeign companyUserFeign;

    @Override
    public void punch(AttendPunchDTO punchDTO) {
        // 1.参数校验
        if (punchDTO == null) {
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }

        // 2.封装打卡信息
        AttendPunch attendPunch = BeanHelper.copyProperties(punchDTO, AttendPunch.class);
        // 2.1 设置基本信息，员工id、公司id、创建时间
        attendPunch.setCompanyUserId(CurrentUserHolder.get().getCompanyUserId());
        attendPunch.setCompanyId(CurrentUserHolder.get().getCompanyId());
        attendPunch.setCreateTime(new Date());

        // 2.2 设置打卡时间（以服务端为准，否则用户可以在客户端修改手机时间打卡）
        Date now = new Date();
        attendPunch.setPunchTime(now);
        attendPunch.setPunchDateStr(DateTimeUtil.dateToStr(now, DateTimeUtil.TIME_FORMAT_2)); // 打卡的年-月-日

        // 2.3 设置上午、下午打卡（1-上午，2-下午）
        int noonType = DateTimeUtil.noonType(now);
        attendPunch.setPunchOnOffWork(noonType + 1);

        // 2.4 查询员工所属的考勤组信息
        AttendGroupDTO attendGroupDTO = attendGroupService.getAttendGroupByUserId();

        // 2.5 判断是否有效范围内打卡，非有效范围抛出异常
        boolean areaFlag = this.validatePunchArea(punchDTO, attendGroupDTO);
        if (!areaFlag) {
            throw new NcException(ResponseEnum.PUNCH_INVALID_AREA);
        }
        attendPunch.setAreaValid(areaFlag);

        // 2.6 判断是否是工作日打卡，非工作日抛出异常
        boolean workDayFlag = this.validateWorkDay(attendGroupDTO);
        if (!workDayFlag) {
            throw new NcException(ResponseEnum.PUNCH_INVALID_DAY);
        }
        attendPunch.setDayType(AttendEnums.DAY_TYPE_WORKDAY.value()); // 工作日

        // 2.7 设置打卡类型（打卡类型 1正常；2迟到；3早退；4旷工）
        this.setPunchType(attendPunch, attendGroupDTO);

        // 2.8 设置是否为有效打卡（上午以最早一次为有效，下午以最晚一次为有效）
        this.validateEffective(attendPunch);

        // 2.9 打卡来源
        attendPunch.setPunchSource(AttendEnums.PUNCH_SOURCE_PUNCH.value());

        attendPunchMapper.insert(attendPunch);
    }

    /**
     * 判断打卡类型 : 1正常；2迟到；3早退；4旷工
     *
     * @param attendPunch
     * @param attendGroupDTO
     */
    private void setPunchType(AttendPunch attendPunch, AttendGroupDTO attendGroupDTO) {
        //获取考勤组中的允许迟到早退分钟数
        Integer allowLateMinutes = attendGroupDTO.getAllowLateMinutes();

        //获取考勤组中的允许旷工分钟数
        Integer lateMinutes = attendGroupDTO.getLateMinutes();

        //获取实际打卡时间
        Date punchTime = attendPunch.getPunchTime();

        //获取实际打卡的时分秒
        String punchTimeStr = DateTimeUtil.dateToStr(punchTime, DateTimeUtil.TIME_FORMAT_1);

        //默认正常
        AttendEnums punchType = AttendEnums.PUNCH_TYPE_OK;

        //判定
        if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) { //上午
            //获取考勤组的设置上班时间（9:00）
            String startWorkTime = attendGroupDTO.getStartWorkTime();

            //判定上班打卡时间
            //返回1 , 说明 : punchTimeStr > startWorkTime + allowLateMinutes（10）
            if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, allowLateMinutes) == 1) {
                punchType = AttendEnums.PUNCH_TYPE_LATE; // 迟到

                //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes（30）
                if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, lateMinutes) == 1) {
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY; // 旷工
                }
            }

            attendPunch.setMorningPunchType(punchType.value());

        } else { //下午
            String offWorkTime = attendGroupDTO.getOffWorkTime();

            //判定下班打卡时间
            //返回1 , 说明 : offWorkTime > punchTimeStr + allowLateMinutes
            if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, allowLateMinutes) == 1) {
                punchType = AttendEnums.PUNCH_TYPE_EARLY; // 早退

                //返回1 , 说明 : offWorkTime > punchTimeStr + lateMinutes
                if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, lateMinutes) == 1) {
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;  // 旷工
                }
            }
            attendPunch.setAfternoonPunchType(punchType.value());
        }
    }

    /**
     * 判断上下午打卡是否有效打卡
     *
     * @param attendPunch
     * @return
     */
    private void validateEffective(AttendPunch attendPunch) {

        //根据条件查询数据库中当前用户的今日上午/下午 打卡记录
        LambdaQueryWrapper<AttendPunch> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendPunch::getCompanyUserId, attendPunch.getCompanyUserId()) //打卡员工ID
                .eq(AttendPunch::getPunchDateStr, attendPunch.getPunchDateStr()) //打卡时间
                .eq(AttendPunch::getPunchOnOffWork, attendPunch.getPunchOnOffWork()) //上午，下午
                .eq(AttendPunch::getAreaValid, true) //打卡范围有效
                .eq(AttendPunch::getPunchSource, AttendEnums.PUNCH_SOURCE_PUNCH.value()); // 打卡来源


        Integer punchCount = attendPunchMapper.selectCount(queryWrapper);
        //如果未获取到, 说明没有打过卡, 此次打卡, 无论是上午还是下午都是有效的
        if (punchCount == 0) {
            attendPunch.setEffectiveValid(true);
        } else {
            //如果获取到记录, 说明已经打过卡
            //如果是上午, 第一条有效, 其余都是无效的
            if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) {
                attendPunch.setEffectiveValid(false);

            } else { //如果是下午, 最后一条有效, 其余都是无效的
                attendPunch.setEffectiveValid(true);
                //更新之前的打卡记录为无效, 以最后一次打卡为准
                AttendPunch ap = new AttendPunch();
                ap.setEffectiveValid(false);
                attendPunchMapper.update(ap, queryWrapper);
            }
        }
    }

    /**
     * 校验工作日
     *
     * @param attendGroupDTO
     * @return
     */
    private boolean validateWorkDay(AttendGroupDTO attendGroupDTO) {
        // 使用hutool的工具类，获取今天是周几
        int week = DateUtil.thisDayOfWeek();   //1: 表示周日, 2: 表示周一, 3:表示周二

        List<String> workdays = attendGroupDTO.getWorkdays(); // [1,1,1,1,1,0,0]

        // 获取在集合中的下标
        if (week == 1) {
            week = 6;

        } else {
            week = week - 2;
        }

        Integer value = Integer.valueOf(workdays.get(week)); // 获取workdays集合元素
        return value == 1;
    }

    /**
     * 校验打卡范围
     *
     * @param attendPunchDTO
     * @param attendGroupDTO
     * @return
     */
    private boolean validatePunchArea(AttendPunchDTO attendPunchDTO, AttendGroupDTO attendGroupDTO) {
        //比较"打卡坐标" 和 "考勤组打卡坐标"的距离
        Integer meter = DistanceUtil.getDistanceMeter(attendPunchDTO.getLat().doubleValue(), attendPunchDTO.getLng().doubleValue(), attendGroupDTO.getLat().doubleValue(), attendGroupDTO.getLng().doubleValue());
        //判定是否小于等于考勤组中设置的范围
        return meter <= attendGroupDTO.getAddressRange();
    }


    @Override
    public List<AttendPunchDTO> queryMembers(String startTime, String endTime) {
        // 1.获取起止日期内(包含起止日期)，所有日期的集合
        List<String> spanDateList = DateTimeUtil.getSpanDateList(startTime, endTime);

        // 2.获取当前企业下所有的员工数据列表
        List<SysCompanyUserDTO> companyUserDTOList = companyUserFeign.queryAllCompanyUser().getData();

        // 3.遍历企业员工列表，将每个员工的DTO 转为 AttendPunchDTO
        List<AttendPunchDTO> punchDTOList = getAttendPunchDTOS(startTime, endTime, spanDateList, companyUserDTOList);

        return punchDTOList;
    }

    /**
     * 公共方法，获取所有员工在某一段时间内的考勤数据
     * @param startTime
     * @param endTime
     * @param spanDateList
     * @param companyUserDTOList
     * @return
     */
    private List<AttendPunchDTO> getAttendPunchDTOS(String startTime, String endTime, List<String> spanDateList, List<SysCompanyUserDTO> companyUserDTOList) {
        List<AttendPunchDTO> punchDTOList = companyUserDTOList.stream().map(companyUserDTO -> {

            // 3.1 封装用户基本信息
            AttendPunchDTO attendPunchDTO = BeanHelper.copyProperties(companyUserDTO, AttendPunchDTO.class);

            // 3.2 获取该用户的指定时间范围内的有效打卡数据
            LambdaQueryWrapper<AttendPunch> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(AttendPunch::getCompanyUserId, companyUserDTO.getId()); // 员工id
            wrapper.between(AttendPunch::getPunchDateStr, startTime, endTime); // 指定时间范围内的打卡数据
            wrapper.eq(AttendPunch::getEffectiveValid, true); // 有效打卡

            // 包含了当前遍历员工的，在某一段时间内的有效打卡数据（每一天的有效打卡数据顶多两条）
            List<AttendPunch> attendPunchList = attendPunchMapper.selectList(wrapper);

            // 将打卡数据进行分组，key为每一天的日期，value为每一天的打卡情况
            Map<String, List<AttendPunch>> map = attendPunchList.stream().collect(
                    Collectors.groupingBy(AttendPunch::getPunchDateStr));


            List<AttendsDTO> attendsList = spanDateList.stream().map(everyDay -> {
                AttendsDTO attendsDTO = new AttendsDTO();
                attendsDTO.setAttendDate(everyDay);

                // 获取遍历这一天的打卡情况
                List<AttendPunch> attendPunches = map.get(everyDay);

                if (CollectionUtils.isEmpty(attendPunches)) { // 代表这一天没有打过卡
                    attendsDTO.setAttendStatus("缺卡/缺卡");
                } else {
                    String handleAttendStatus = handleAttendStatus(attendPunches);
                    attendsDTO.setAttendStatus(handleAttendStatus);
                }
                return attendsDTO;

            }).collect(Collectors.toList());

            attendPunchDTO.setAttendsList(attendsList);

            return attendPunchDTO;

        }).collect(Collectors.toList());
        return punchDTOList;
    }


    /**
     * 处理打卡状态
     * @param punches
     * @return
     */
    private String handleAttendStatus(List<AttendPunch> punches) {
        String amAttendStatus = "缺卡"; //上午 - 考勤
        String pmAttendStatus = "缺卡"; //下午 - 考勤

        for (AttendPunch punch : punches) {
            if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) {
                amAttendStatus = this.getPuchTypeDesc(punch.getMorningPunchType());
            } else if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_AFTERNOON.value()) {
                pmAttendStatus = this.getPuchTypeDesc(punch.getAfternoonPunchType());
            }
        }
        return amAttendStatus + "/" + pmAttendStatus;
    }

    /**
     * 根据打卡类型，获取打卡类型描述
     */
    private String getPuchTypeDesc(Integer punchType) {
        String desc = "";
        switch (punchType) {
            case 1:
                desc = AttendEnums.PUNCH_TYPE_OK.toString();
                break;
            case 2:
                desc = AttendEnums.PUNCH_TYPE_LATE.toString();
                break;
            case 3:
                desc = AttendEnums.PUNCH_TYPE_EARLY.toString();
                break;
            case 4:
                desc = AttendEnums.PUNCH_TYPE_STAYALAY.toString();
                break;
            case 5:
                desc = AttendEnums.PUNCH_TYPE_NOT_WORK_DAY.toString();
                break;
            default:
                break;
        }
        return desc;
    }

    @Override
    public void export(String startTime, String endTime) throws Exception{
        // 1.根据传入的startTime、endTime，查询时间段内的考勤数据（已完成）
        List<AttendPunchDTO> punchDTOList = this.queryMembers(startTime, endTime);

        // 2.获取响应对象response，设置文件下载的头信息;
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = servletRequestAttributes.getResponse();
        response.setHeader("Content-disposition", "attachment;filename=Attendance.xls");
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setCharacterEncoding("utf-8");


        //3.通过response获取输出流, 并生成的Excel输出
        EasyExcel.write(response.getOutputStream())
                .excelType(ExcelTypeEnum.XLS)
                .head(handleHeaders(punchDTOList)) // 构造表头信息
                .registerWriteHandler(horizontalCellStyleStrategy())
                .registerWriteHandler(new CustomHandler())
                .sheet("考勤数据")
                .doWrite(handleData(punchDTOList)); // 构造每一行的数据

    }

    @Override
    public String generatePunchReport(Long companyId) {
        // 1.获取当月第一天到最后一天的日期集合
        String startTime = DateTimeUtil.getCurrentMonthFirstDay();
        String endTime = DateTimeUtil.getCurrentMonthLastDay();
        List<String> spanDateList = DateTimeUtil.getSpanDateList(startTime, endTime);

        // 2.获取企业的员工列表
        List<SysCompanyUserDTO> companyUserDTOS = companyUserFeign.queryAllUserByCompanyId(companyId).getData();

        // 3.调用公共方法，获取考勤数据
        List<AttendPunchDTO> attendPunchDTOS = getAttendPunchDTOS(startTime, endTime, spanDateList, companyUserDTOS);

        // 4.调用EasyExcel生成报表
        // 4.1 拼接考勤报表存储目录
        String folderName = "D:" + File.separator + "考勤报表" + File.separator + companyId;

        File folder = new File(folderName);

        if(!folder.exists()){
            // 如果文件夹不存在，先创建文件夹
            folder.mkdirs();
        }

        // 4.2 拼接文件名称，D:\考勤数据\公司id\xxx公司-2021-11-考勤数据.xls
        String filePath = folderName + File.separator +
                companyUserDTOS.get(0).getCompanyName() + "-" + startTime.substring(0, 7) + "-考勤数据.xls";

        EasyExcel.write(new File(filePath))
                .excelType(ExcelTypeEnum.XLS)
                .head(handleHeaders(attendPunchDTOS)) // 构造表头信息
                .registerWriteHandler(horizontalCellStyleStrategy())
                .registerWriteHandler(new CustomHandler())
                .sheet("考勤数据")
                .doWrite(handleData(attendPunchDTOS)); // 构造每一行的数据


        return filePath;
    }

    /**
     * 构造每一行数据
     * @param punchDTOList
     * @return
     */
    private List<List<String>> handleData(List<AttendPunchDTO> punchDTOList) {

        List<List<String>> dataList = new ArrayList<>();

        if(!CollectionUtils.isEmpty(punchDTOList)){
            // 遍历每一行数据
            for (AttendPunchDTO attendPunchDTO : punchDTOList) {

                // 构造每一行数据
                List<String> row = new ArrayList<>();

                row.add(attendPunchDTO.getUserName()); // 姓名
                row.add(attendPunchDTO.getWorkNumber()); // 工号
                row.add(attendPunchDTO.getDepartmentName()); // 部门
                row.add(attendPunchDTO.getPost()); // 职位

                // 获取当前数据中每一天的考勤状态
                List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();

                if(!CollectionUtils.isEmpty(attendsList)){
                    for (AttendsDTO attendsDTO : attendsList) {
                        row.add(attendsDTO.getAttendStatus());
                    }
                }

                // 将每一行数据，添加到dataList
                dataList.add(row);
            }
        }
        return dataList;
    }

    /**
     * 构造头信息
     * @param punchDTOList
     * @return
     */
    private List<List<String>> handleHeaders(List<AttendPunchDTO> punchDTOList) {
        List<List<String>> headers = new ArrayList<>();
        headers.add(Arrays.asList("姓名"));
        headers.add(Arrays.asList("工号"));
        headers.add(Arrays.asList("部门"));
        headers.add(Arrays.asList("职位"));

        if(!CollectionUtils.isEmpty(punchDTOList)){
            // 获取第一条数据
            AttendPunchDTO attendPunchDTO = punchDTOList.get(0);

            // 获取一共有多少天的考勤数据
            List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();

            if(!CollectionUtils.isEmpty(attendsList)){

                for (AttendsDTO attendsDTO : attendsList) {
                    headers.add(Arrays.asList(attendsDTO.getAttendDate()));
                }
            }
            
        }
        return headers;
    }

    /**
     * 设置样式
     */
    public HorizontalCellStyleStrategy horizontalCellStyleStrategy() {

        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //内容策略 - 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

        //设置字体
        WriteFont font = new WriteFont();
        font.setFontName("阿里巴巴普惠体");
        font.setFontHeightInPoints((short) 11);
        contentWriteCellStyle.setWriteFont(font);
        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(contentWriteCellStyle, contentWriteCellStyle);

        return horizontalCellStyleStrategy;
    }
}
